DAY29有提到一個東西 Context
Context是什麼呢?
Golang v1.7之後併入標準庫內,去google出來的context好像強國文章大部份都叫「上下文」,連翻譯叫都「上下文」,雖然上下文的意境好像有點沒錯啦,因為context主要是拿來做goroutine間的控管。
有興趣可以參考這幾篇文章
上下文 Context
深度解密Go语言之context
Goroutine對go來說是個使用上很方便,管理起來很屎尿的東西,
Goroutine可以包Goroutine,
像以下的範例,雖然很爛...
func main() {
for i := 0; i < 10; i++ {
ii := i
fmt.Print("i:", ii)
go func() {
for j := 0; j < 20; j++ {
jj := j
fmt.Print("i j:", ii, jj)
go func() {
time.Sleep(1 * time.Second)
fmt.Print("GG")
}()
}
}()
}
for {
}
}
上面的例子會同時併發200個Goroutine出去處理事情,雖然可以使用channel管理,但是一但遇到多層的併發狀態時,要進行狀態管理是很無解的,就跟KD勇一樣無解
要如何使用context呢
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/DoGetByQueryString", func(c *gin.Context) {
time.Sleep(20 * time.Second)
p1 := c.DefaultQuery("param1", "Default")
p2 := c.Query("param2")
c.JSON(http.StatusOK, gin.H{"param1": p1, "param2": p2})
})
srv := &http.Server{
Addr: ":8787",
Handler: router,
}
ch := make(chan os.Signal, 1)
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Println("SERVER GG惹:", err)
}
}()
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
<-ch
log.Println("context.WithDeadline start:", time.Now())
//收到關閉訊息後,設定WithDeadline為20秒後,20秒後才會發出context.Done的訊息出來
c, cancel := context.WithDeadline(context.Background(), time.Now().Add(20*time.Second))
defer cancel()
if err := srv.Shutdown(c); err != nil {
log.Println("srv.Shutdown:", err)
}
select {
case <-c.Done():
fmt.Println("context.WithDeadline done:", time.Now(), c.Err().Error())
close(ch)
}
}
執行結果,二個時間區間正好20秒
2020/10/06 11:26:39 context.WithDeadline start: 2020-10-06 11:26:39.157852 +0800 CST m=+8.180100057
context.WithDeadline done: 2020-10-06 11:26:59.16354 +0800 CST m=+28.185433792 context deadline exceeded
把WithDeadline的code改成下面這句,5秒後就強制停止
c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
執行結果,差五秒
C2020/10/06 11:31:45 context.WithTimeout start: 2020-10-06 11:31:45.65742 +0800 CST m=+4.507931447
context.WithTimeout done: 2020-10-06 11:31:50.660076 +0800 CST m=+9.510437665 context deadline exceeded
基本上WithDeadline跟WithTimeout可以視為相同的東西,只是WithDeadline設定的時間,所以要考慮到時區問題,這樣子反而使用WithTimeout還比較安全
因為context是個樹狀的結構,主線程的Parent context可以產生子context,子context又可以繼續生成下層context,使用WithCancel產生子context時,這可以透過context.Cancel()發出終止訊息,讓繼承出來的context一起收到終止訊息
使用上面的code來範例
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/DoGetByQueryString", func(c *gin.Context) {
time.Sleep(20 * time.Second)
p1 := c.DefaultQuery("param1", "Default")
p2 := c.Query("param2")
c.JSON(http.StatusOK, gin.H{"param1": p1, "param2": p2})
})
srv := &http.Server{
Addr: ":8787",
Handler: router,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Println("SERVER GG惹:", err)
}
}()
//產生一個屬於WithCancel的子context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
select {
//聽到關閉訊號就對ctx進行關閉
case <-ch:
log.Println("listen SIGTERM:", time.Now())
close(ch)
cancel()
}
}()
<-ctx.Done()
//繼承上層context產生一個WithTimeout的context,設定5秒後timeout
log.Println("context.WithTimeout start:", time.Now())
c, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if err := srv.Shutdown(c); err != nil {
log.Println("srv.Shutdown:", err)
}
select {
case <-c.Done():
fmt.Println("context.WithTimeout done:", time.Now(), c.Err().Error())
}
}
執行結果:因為上層的context聽到關閉訊號,馬上對下層context發出context.Done(),這時候就算是使用WithTimeout context的srv.Shutdown也會跟著被影響到,馬上進行關閉
2020/10/06 12:05:25 listen SIGTERM: 2020-10-06 12:05:25.046776 +0800 CST m=+5.484417516
2020/10/06 12:05:25 context.WithTimeout start: 2020-10-06 12:05:25.047202 +0800 CST m=+5.484843131
//srv.Shutdown()發生錯誤了
2020/10/06 12:05:25 srv.Shutdown: context canceled
context.WithTimeout done: 2020-10-06 12:05:25.047311 +0800 CST m=+5.484952722 context canceled
因為context是thread safe,所以Goroutine間對context進行WithValue是不會噴data race的,
WithValue主要是做值的傳遞使用
雖然參數2定義上是interface{},但是使用string當key值時,go-lint會提示,執行上還是沒問題
should not use basic type string as key in context.WithValue
參考官方寫法的話,先自定義一個type,再宣告一個自定義type的變數來當key值
type contextKey string
key := contextKey("test")
ctx := context.WithValue(context.Background(), key, "i'm test")
fmt.Println(ctx.Value(key))
WithValue產生的context再產生下層context時,會把塞好的value傳遞給下層context中
type contextKey string
key := contextKey("test")
//產生一個context帶有{"test":"i'm test"}的值
ctx := context.WithValue(context.Background(), key, "i'm test")
fmt.Println(ctx.Value(key))
//繼承ctx產生一個子context
ctx1, cancel := context.WithCancel(ctx)
defer cancel()
fmt.Println(ctx1.Value(key))
執行結果,驗證了透過WithValue可以把值傳遞給context衍生出來子context,好像拿來做trace_id使用應該不錯,每次的request都是一個context,如果用這方式就log這個request的足跡
i'm test
i'm test
原文在這 Overview第五段
func DoSomethingWrong(ctx context.Context) error {
}
Golang是第一次接觸的程式語言,雖然有些地方在其他程式也有看到,但是感覺Golang就是一個更輕量化的程式語言(望著c#),要寫個http的服務真的程式少少幾行就可以run起來,加上萬惡的Goroutine,用過之後就回不去了。
寫了鐵人賽之後才回看去看看Gin與Mux的middleware在幹嘛,原來套件已經提供了很好的middleware func可以直接使用,不需要再自己硬刻code。
雖然寫30天真的很硬又很血汗,一邊工作在爆,還要回家刻文章,想想還真的頗痛苦,但是說真的,寫完才知道之前不懂的東西,居然在寫完文章後就最少能懂個皮毛,算是參賽最大的收獲吧,也感謝寫Golang文章的前輩,拜讀完才能促使自己寫下文章,文章還有很多不足的地方,也請閱讀的人請多多包涵~~感謝~